5. 실행 환경 작성에 사용하는 기반 기술
실행 환경 작성에 사용하는 기반 기술
컨테이너가 호스트와 격리된 실행 환경을 만들기 위해 리눅스 커널이 제공하는 네임스페이스와 cgroup 기능을 사용함.
- 격리(Isolation): 프로세스가 다른 프로세스나 시스템 자원에 접근하지 못하도록 분리하는 것. 컨테이너의 핵심 개념
- 리눅스 커널(Linux Kernel): 운영체제의 핵심 부분으로, 하드웨어와 소프트웨어 사이를 중재하며 프로세스, 메모리, 파일시스템 등을 관리
- 네임스페이스(Namespace): 프로세스가 볼 수 있는 시스템 자원을 격리하는 Linux 커널 기능
- cgroup(Control Groups): 프로세스의 자원(CPU, 메모리 등) 사용량을 제한하는 Linux 커널 기능
1. 네임스페이스 (Namespace)
어떤 프로세스에서 조작할 수 있는 리소스를 다른 프로세스와 격리할 수 있는 기능임. 컨테이너는 일반적으로 여러 네임스페이스를 조합해서 실행 환경을 작성함.
네임스페이스 종류
| 네임스페이스 | 설명 | 격리 대상 |
|---|---|---|
| PID | 프로세스 ID 격리 | 프로세스 목록, PID 번호 |
| Mount | 마운트 포인트 격리 | 파일시스템 마운트 목록 |
| Network | 네트워크 리소스 격리 | 네트워크 장치, IP, 포트 |
| UTS | 호스트명 격리 | hostname, domainname |
| IPC | 프로세스 간 통신 격리 | 공유 메모리, 세마포어, 메시지 큐 |
| User | 사용자/그룹 격리 | UID/GID 매핑, 권한 |
- PID(Process ID): 운영체제가 각 프로세스를 식별하기 위해 부여하는 고유 번호
- 마운트 포인트(Mount Point): 파일시스템을 디렉터리 트리에 연결하는 지점. 예:
/mnt/usb - UTS(Unix Time-sharing System): 호스트명과 도메인명을 관리하는 시스템 식별자
- IPC(Inter-Process Communication): 프로세스 간 데이터를 주고받는 통신 메커니즘. 공유 메모리, 세마포어, 메시지 큐 등
- UID/GID: User ID/Group ID. 사용자와 그룹을 식별하는 숫자 값
네임스페이스 격리 구조
┌─────────────────────────────────────────────────────────────┐
│ 호스트 (Host) │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 프로세스 A │ │ 프로세스 B │ │
│ │ PID: 1234 │ │ PID: 5678 │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 컨테이너 (격리된 네임스페이스) │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ 프로세스 C │ │ 프로세스 D │ │ │
│ │ │ PID: 1 │ │ PID: 2 │ │ │
│ │ │ (호스트: 9999) │ │ (호스트: 10000) │ │ │
│ │ └─────────────────┘ └─────────────────┘ │ │
│ │ • 호스트 프로세스 보이지 않음 │ │
│ │ • PID 1부터 새로 부여 │ │
│ │ • 독립된 네트워크 스택 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
2. unshare 명령어로 격리 환경 만들기
unshare 명령어를 사용해서 도커나 runc가 만드는 컨테이너처럼 호스트와 격리된 실행 환경을 작성할 수 있음.
- unshare: 새로운 네임스페이스를 생성하고 그 안에서 프로그램을 실행하는 Linux 명령어
- chroot(Change Root): 프로세스의 루트 디렉터리를 변경하는 명령어. 파일시스템 격리의 기초 기술
- fork: 현재 프로세스를 복제하여 새로운 자식 프로세스를 생성하는 시스템 콜
기본 사용법
# 새로운 네임스페이스에서 셸 실행
unshare -fpmn chroot bundle/rootfs /bin/sh
# 격리된 환경에서 OS 정보 확인
cat /etc/os-release
unshare 옵션
| 옵션 | 의미 | 설명 |
|---|---|---|
-f |
fork | 새 프로세스로 명령어 실행 |
-p |
PID namespace | PID 네임스페이스 생성 |
-m |
Mount namespace | 마운트 네임스페이스 생성 |
-n |
Network namespace | 네트워크 네임스페이스 생성 |
3. PID Namespace
-p 옵션으로 PID 네임스페이스를 생성하면 새로운 네임스페이스에서는 PID가 1부터 다시 부여됨.
- procfs(/proc): 커널이 제공하는 가상 파일시스템. 프로세스 정보, 시스템 상태 등을 파일 형태로 제공
- PID 1(init 프로세스): 시스템의 첫 번째 프로세스. 모든 프로세스의 조상이며, 고아 프로세스를 거둬들이는 역할 수행
PID 격리 동작
# 새 네임스페이스에서 procfs 마운트 후 프로세스 확인
mount -t proc proc /proc
ps -Ao pid,args
# 출력 예시 - 호스트 프로세스는 보이지 않음
PID COMMAND
1 /bin/sh ← unshare로 실행한 셸이 PID 1
2 ps -Ao pid,args
이중 PID 구조
새 네임스페이스 안에서 생성된 프로세스는 최소 2개의 PID를 가짐:
┌──────────────────────────────────────────────────┐
│ 호스트 네임스페이스 │
│ sleep 12345 → PID: 9876 │
└──────────────────────────────────────────────────┘
↑
같은 프로세스
↓
┌──────────────────────────────────────────────────┐
│ 컨테이너 네임스페이스 │
│ sleep 12345 → PID: 5 │
└──────────────────────────────────────────────────┘
# 네임스페이스 내부에서 실행
sleep 12345 &
ps -Ao pid,args | grep "sleep 12345"
# 출력: 5 sleep 12345
# 호스트에서 실행
ps -Ao pid,cmd | grep "sleep 12345"
# 출력: 9876 sleep 12345
4. Mount Namespace
-m 옵션으로 마운트 네임스페이스를 생성하면 마운트/언마운트 작업이 호스트에 영향을 주지 않음.
마운트 격리 동작
┌────────────────────────────────────────────────────────┐
│ 호스트 │
│ /proc → 호스트 procfs │
│ bundle/rootfs/proc → (빈 디렉터리) │
└────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────┐
│ 컨테이너 (Mount Namespace) │
│ /proc → 컨테이너 procfs ← 호스트에서 보이지 않음 │
└────────────────────────────────────────────────────────┘
runc의 루트 디렉터리 변경
- 예제에서는
chroot를 사용했지만, runc는pivot_root(2)시스템 콜 사용 - 마운트 네임스페이스 덕분에 루트 디렉터리 변경이 시스템 전체에 영향을 주지 않음
- pivot_root: 현재 루트 파일시스템을 새로운 위치로 이동하고, 다른 파일시스템을 새 루트로 설정하는 시스템 콜. chroot보다 보안성이 높음
- 시스템 콜(System Call): 사용자 공간 프로그램이 커널 기능을 요청하는 인터페이스. 예: read, write, clone, mount
shared subtree
네임스페이스끼리 마운트/언마운트 이벤트를 공유할 수 있는 기능도 있음.
- shared subtree: 마운트 네임스페이스 간에 마운트 이벤트를 전파하거나 격리하는 기능. shared, private, slave, unbindable 네 가지 모드 존재
5. Network Namespace
-n 옵션으로 네트워크 네임스페이스를 생성하면 네트워크 장치가 호스트와 격리됨.
네트워크 격리 동작
# 컨테이너 내부에서 실행
ip a
# 출력: loopback만 표시, 호스트 네트워크 장치 없음
# 호스트에서 실행
ip a
# 출력: eth0, docker0 등 모든 네트워크 장치 표시
컨테이너 네트워킹 구성
도커나 CNI 플러그인이 새 네트워크 네임스페이스에 가상 네트워크 인터페이스를 생성해서 통신 가능하게 함.
- CNI(Container Network Interface): 컨테이너 네트워킹을 위한 표준 인터페이스. 쿠버네티스 등에서 네트워크 플러그인 연결에 사용
- veth(Virtual Ethernet): 한 쌍으로 생성되는 가상 네트워크 인터페이스. 한쪽에서 보낸 패킷이 다른 쪽으로 전달됨
- 브릿지(Bridge): 여러 네트워크 인터페이스를 하나의 네트워크 세그먼트로 연결하는 가상 스위치. docker0가 대표적
- loopback(lo): 자기 자신과 통신하기 위한 가상 네트워크 인터페이스. IP 주소 127.0.0.1
┌─────────────────────────────────────────────────────────┐
│ 호스트 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ eth0 │ │ docker0 │ │ vethXXX │ │
│ │ (물리) │ │ (브릿지) │←────→│ (가상) │ │
│ └─────────┘ └─────────┘ └────┬────┘ │
│ │ │
│ ┌─────────────────────────────────┼──────┐ │
│ │ 컨테이너 (Network NS) │ │ │
│ │ ┌──────┴────┐ │ │
│ │ │ eth0 │ │ │
│ │ │ (가상) │ │ │
│ │ └───────────┘ │ │
│ └────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
추상 유닉스 소켓 격리
| 소켓 유형 | 경로 예시 | 접근 관리 |
|---|---|---|
| 일반 유닉스 소켓 | /run/docker.sock |
파일시스템 권한으로 관리 |
| 추상 유닉스 소켓 | @/tmp/.socket |
경로 없음, 관리 어려움 |
- 유닉스 소켓(Unix Socket): 같은 시스템 내 프로세스 간 통신에 사용하는 소켓. 파일시스템 경로로 식별
- 추상 유닉스 소켓(Abstract Unix Socket): 파일시스템에 존재하지 않는 유닉스 소켓.
@로 시작하며, 파일 권한으로 접근 제어 불가
네트워크 네임스페이스로 추상 유닉스 소켓을 격리할 수 있음:
# 호스트에서 실행 - 추상 소켓 목록 표시
grep -ao '@.*' /proc/net/unix
# 컨테이너에서 실행 - 출력 없음 (격리됨)
grep -ao '@.*' /proc/net/unix
6. cgroup (Control Groups)
프로세스가 사용할 수 있는 리소스를 제한하는 기능임.
- 컨트롤러(Controller): cgroup에서 특정 자원 유형을 관리하는 모듈. cpu, memory, io 등 각각의 자원별로 존재
- 계층 구조(Hierarchy): 부모-자식 관계로 구성된 트리 형태의 구조. cgroup은 계층적으로 자원 제한을 상속
cgroup으로 제어 가능한 리소스
| 컨트롤러 | 제어 대상 |
|---|---|
| devices | 디바이스 파일 접근 권한 |
| cpu | CPU 사용량 제한 |
| memory | 메모리 사용량 제한 |
| io | I/O 대역폭 제한 |
| pids | 프로세스 개수 제한 |
cgroup 계층 구조
/sys/fs/cgroup/
├── cgroup.controllers ← 사용 가능한 컨트롤러 목록
├── cgroup.procs ← 소속 프로세스 목록
├── cpu.max ← CPU 제한 설정
├── memory.max ← 메모리 제한 설정
│
├── docker/ ← 도커 컨테이너용 cgroup
│ ├── container_id_1/
│ │ ├── cgroup.procs
│ │ └── memory.max
│ └── container_id_2/
│
└── system.slice/ ← systemd 관리 cgroup
└── unshare_demo.scope/
핵심 특징: 루트에서 말단으로 갈수록 제한이 강해지는 구조
7. cgroup v1 vs v2
버전별 특징 비교
| 특징 | cgroup v1 | cgroup v2 |
|---|---|---|
| 파일시스템 구조 | 컨트롤러마다 분리 | 단일 통합 계층 구조 |
| 프로세스 소속 | 중간 cgroup 소속 가능 | 루트 또는 말단만 가능 |
| 디바이스 제어 | devices.deny 파일 사용 |
eBPF 프로그램 사용 |
| 설계 철학 | 유연함 (복잡함) | 단순함 (일관성) |
| 지원 배포판 | Ubuntu 20.04 이하 | Ubuntu 22.04+, 최신 배포판 |
- eBPF(extended Berkeley Packet Filter): 리눅스 커널 내에서 샌드박스된 프로그램을 실행하는 기술. 네트워크 필터링, 트레이싱, 보안 정책 등에 활용
- 말단 cgroup(Leaf cgroup): 자식 cgroup이 없는 최하위 cgroup. cgroup v2에서는 프로세스가 말단 cgroup에만 소속 가능
cgroup v1 디렉터리 구조
# Ubuntu 20.04 (cgroup v1)
ls /sys/fs/cgroup/
# 출력: blkio cpu cpuacct cpuset devices freezer memory net_cls ...
컨트롤러마다 별도 디렉터리가 존재함.
cgroup v2 디렉터리 구조
# Ubuntu 22.04 (cgroup v2)
ls /sys/fs/cgroup/
# 출력: cgroup.controllers cgroup.procs cpu.max memory.max io.max ...
cat /sys/fs/cgroup/cgroup.controllers
# 출력: cpuset cpu io memory pids
모든 컨트롤러가 단일 디렉터리에서 관리됨.
8. cgroup v1 디바이스 접근 제한 예시
시나리오
unshare로 만든 격리 환경에서 /dev/sda 디바이스 접근을 차단함.
- 디바이스 노드(Device Node):
/dev디렉터리 아래의 특수 파일. 하드웨어 장치를 파일처럼 접근할 수 있게 함 - 블록 디바이스(Block Device): 데이터를 블록 단위로 읽고 쓰는 저장 장치. 예: HDD(/dev/sda), SSD
- 메이저/마이너 번호(Major/Minor Number): 디바이스를 식별하는 숫자 쌍. 메이저는 드라이버 유형, 마이너는 해당 드라이버 내 개별 장치
- mknod: 디바이스 노드 파일을 생성하는 명령어
- hexdump: 파일이나 장치의 내용을 16진수로 표시하는 명령어
제한 전 동작 확인
# 격리 환경 내부에서 디바이스 노드 생성
mknod /dev/sda b 8 0
# 디스크 읽기 가능
hexdump -n 4 /dev/sda
# 출력: 0000000 63eb 0090
cgroup 설정으로 접근 차단
# 1. 새 cgroup 생성
mkdir /sys/fs/cgroup/devices/unshare_demo
# 2. /dev/sda 읽기/쓰기 거부 설정
# b=블록디바이스, 8:0=메이저:마이너 번호, rw=읽기쓰기 거부
echo "b 8:0 rw" > /sys/fs/cgroup/devices/unshare_demo/devices.deny
# 3. 셸 프로세스를 cgroup에 소속
echo 4423 > /sys/fs/cgroup/devices/unshare_demo/cgroup.procs
제한 후 동작 확인
# 디스크 읽기 불가능
hexdump -n 4 /dev/sda
# 출력: hexdump: /dev/sda: Operation not permitted
도커 컨테이너의 devices cgroup
docker run -it --rm busybox:1.31 /bin/sh
# 컨테이너 내부에서 확인
cat /sys/fs/cgroup/devices/devices.list
# 출력: 허용된 디바이스 목록 표시
9. cgroup v2 디바이스 접근 제한 예시
cgroup v2에서는 eBPF를 사용해서 디바이스 접근을 제어함.
eBPF란?
리눅스 커널의 프로그램 실행 환경으로, 커널 동작을 다양하게 변경/확장할 수 있음:
- 시스템 콜 트레이싱
- 네트워크 패킷 처리
- cgroup 디바이스 제어
- 트레이싱(Tracing): 프로그램 실행 흐름이나 시스템 호출을 추적하고 기록하는 것. 디버깅과 성능 분석에 활용
- systemd: 리눅스 시스템의 서비스 관리자(init 시스템). 서비스 시작/종료, cgroup 관리 등 수행
- systemd-run: systemd를 통해 임시로 서비스나 스코프를 생성하여 명령어를 실행하는 도구
- DeviceAllow: systemd 유닛 설정에서 디바이스 접근 권한을 지정하는 옵션
systemd를 사용한 디바이스 제한
# 제한 전 - 디스크 읽기 가능
mknod /dev/sda b 8 0
hexdump -n 4 /dev/sda
# 출력: 0000000 63eb 0090
# DeviceAllow로 제한 설정하면서 실행환경 생성
# m = 디바이스 파일 생성만 허가 (읽기/쓰기 거부)
systemd-run \
-p "DeviceAllow=/dev/sda m" \
--unit=unshare_demo \
--scope \
unshare -fpmn chroot bundle/rootfs /bin/sh
# 제한 후 - 디스크 읽기 불가능
mknod /dev/sda b 8 0
hexdump -n 4 /dev/sda
# 출력: hexdump: /dev/sda: Operation not permitted
cgroup 정보 확인
# 생성된 cgroup 확인
systemd-cgls -u unshare_demo.scope
# cgroup 디렉터리 위치
cat /sys/fs/cgroup/system.slice/unshare_demo.scope/cgroup.procs
# 출력: 3640 3641
ps -o pid,args -p "3640 3641"
# 출력:
# PID ARGS
# 3640 unshare -fpmn chroot bundle/rootfs /bin/sh
# 3641 /bin/sh
10. cgroup namespace와 nsdelegate
cgroup namespace
프로세스에서 cgroup 계층 구조를 한정적인 범위로 보여주는 기능임.
- cgroup namespace: 프로세스가 볼 수 있는 cgroup 계층 구조를 격리하는 네임스페이스. 컨테이너가 자신의 cgroup을 루트로 인식하게 함
- nsdelegate: cgroup v2의 마운트 옵션으로, cgroup namespace 경계를 넘는 자원 위임을 제한하는 보안 기능
nsdelegate (cgroup v2)
cgroup v2에 도입된 보안 기능:
| 기능 | 설명 |
|---|---|
| 프로세스 이동 제한 | namespace 내부 프로세스를 다른 cgroup으로 이동 불가 |
| 설정 파일 쓰기 제한 | namespace 외부에서 설정 변경 제한 |
요약
┌─────────────────────────────────────────────────────────────┐
│ 컨테이너 격리 기술 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 네임스페이스 (Namespace) cgroup │
│ ━━━━━━━━━━━━━━━━━━━━━ ━━━━━━ │
│ "무엇을 볼 수 있는가" "얼마나 쓸 수 있는가" │
│ │
│ • PID - 프로세스 격리 • cpu - CPU 제한 │
│ • Mount - 파일시스템 격리 • memory - 메모리 제한 │
│ • Network - 네트워크 격리 • io - I/O 제한 │
│ • UTS - 호스트명 격리 • devices - 디바이스 제한 │
│ • IPC - IPC 격리 • pids - 프로세스 수 제한 │
│ • User - 사용자 격리 │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ 컨테이너 런타임 (runc, containerd, CRI-O) │
│ ↓ │
│ 네임스페이스 + cgroup 조합으로 컨테이너 생성 │
│ │
└─────────────────────────────────────────────────────────────┘
| 개념 | 역할 | 구현 방식 |
|---|---|---|
| 네임스페이스 | 리소스 가시성 격리 | unshare, clone() 시스템 콜 |
| cgroup | 리소스 사용량 제한 | 파일시스템 기반 설정 |
| chroot/pivot_root | 루트 디렉터리 격리 | 시스템 콜 |
| eBPF | 커널 동작 확장 (v2) | 커널 내 프로그램 실행 |
참고 자료
공식 문서:
- Linux Namespaces: https://man7.org/linux/man-pages/man7/namespaces.7.html
- cgroups v2: https://docs.kernel.org/admin-guide/cgroup-v2.html
- eBPF: https://ebpf.io/
심화 자료:
- Linux Containers from Scratch: https://ericchiang.github.io/post/containers-from-scratch/
- Container Security: https://kubernetes.io/docs/concepts/security/